//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
//
//	Project:	Integer Array Statistics GUI
//	Author:		Nick Foltz, Nick Sampanis
//	Created:	June 29, 2010
//	License:	Unspecified
//
//	Description
//		Uses a text change-driven text input box to get a set of integers
//		Strips the set into a valid list of integers
//		Gathers statistics on the list and updates labels with results
//
//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/

package arrayStatistics;

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.text.*;

import java.util.*;
import java.util.regex.*;

public class ArrayStatistics extends JFrame {
	// GUI component variables
	JTextField inputBox;
	private JLabel inputInstructions;
	private JLabel outputSum;
	private JLabel outputMax;
	private JLabel outputMin;
	private JLabel outputAverage;
	private JLabel outputMedian;
	
	public ArrayStatistics() {
		// Choose our layout manager
		GroupLayout layout = new GroupLayout(getContentPane());
		getContentPane().setLayout(layout);
		
		// Specify automatic gap creation
		layout.setAutoCreateGaps(true);
		layout.setAutoCreateContainerGaps(true);
		
		// Initialize GUI component variables
		inputBox = new JTextField();
		inputInstructions = new JLabel("Enter a comma-separated list of integers.");
		outputSum = new JLabel();
		outputMax = new JLabel();
		outputMin = new JLabel();
		outputAverage = new JLabel();
		outputMedian = new JLabel();
		blank();
		
		// Set up the regex filter
		AbstractDocument abstractDoc;
		Document normalDoc = inputBox.getDocument();
		if (normalDoc instanceof AbstractDocument) {
			abstractDoc = (AbstractDocument)normalDoc;
			abstractDoc.setDocumentFilter(new DocumentRegexFilter());
		}
		
		// Set up an event for changes in text
		inputBox.getDocument().addDocumentListener(new MyDocumentListener());
		
		// Organize GUI components
		// Align all components' left edges vertically
		layout.setHorizontalGroup(
			layout.createParallelGroup(GroupLayout.Alignment.LEADING)
				.addComponent(inputBox)
				.addComponent(inputInstructions)
				.addComponent(outputSum)
				.addComponent(outputMax)
				.addComponent(outputMin)
				.addComponent(outputAverage)
				.addComponent(outputMedian)
		);
		// Add all components to a plain sequential vertical group
		// Prevent the input box from being resized vertically
		layout.setVerticalGroup(
			layout.createSequentialGroup()
				.addComponent(inputBox, GroupLayout.PREFERRED_SIZE, GroupLayout.DEFAULT_SIZE, GroupLayout.PREFERRED_SIZE)
				.addComponent(inputInstructions)
				.addGap(20)
				.addComponent(outputSum)
				.addComponent(outputMax)
				.addComponent(outputMin)
				.addComponent(outputAverage)
				.addComponent(outputMedian)
		);
	}
	
	// Clear the labels
	private void blank() {
		outputSum.setText("Sum: ");
    	outputMax.setText("Max: ");
    	outputMin.setText("Min: ");
    	outputAverage.setText("Average: ");
    	outputMedian.setText("Median: ");
	}
	
	// This filter blocks undesirable text entry.
	// "Everybody stand back. I know regular expressions."
	public class DocumentRegexFilter extends DocumentFilter {
		// Reject all symbols that are not integers or commas
		// Ensure no stray or multiple commas
		// Eliminate integer values too excessively large
		String rejectPattern = "^,|[^0-9,-]|,,|[1-9][0-9]{10,}|-[^1-9]|[0-9]-";
		Pattern pattern = Pattern.compile(rejectPattern);
		
		// Override function to back out if a change hits our filter
		public void insertString(DocumentFilter.FilterBypass fb, int offset, String text, AttributeSet attr) {
			String oldText = inputBox.getText();
			String newText;
			// Make a copy of what the new text would look like
			if (oldText.length() > 0) {
				newText = oldText.substring(0, offset).concat(text).concat(oldText.substring(offset));
			} else {
				newText = text;
			}
			// Regex match against our pattern
			Matcher matcher = pattern.matcher(newText);
			if (!matcher.find()) {
				// Go ahead with the insert
				try {
					super.insertString(fb, offset, text, attr);
				} catch (BadLocationException e) {
					// Shouldn't happen
					e.printStackTrace();
				}
			}
		}
		// Override function to back out if a change hits our filter
		public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String text, AttributeSet attr) {
			String oldText = inputBox.getText();
			String newText;
			// Make a copy of what the new text would look like
			if (oldText.length() > 0) {
				newText = oldText.substring(0, offset).concat(text).concat(oldText.substring(offset + length));
			} else {
				newText = text;
			}
			// Regex match against our pattern
			Matcher matcher = pattern.matcher(newText);
			if (!matcher.find()) {
				// Go ahead with the replace
				try {
					super.replace(fb, offset, length, text, attr);
				} catch (BadLocationException e) {
					// Shouldn't happen
					e.printStackTrace();
				}
			}
		}
		// Override function to back out if a change hits our filter
		public void remove(DocumentFilter.FilterBypass fb, int offset, int length) {
			String oldText = inputBox.getText();
			String newText;
			// Make a copy of what the new text would look like
			newText = oldText.substring(0, offset).concat(oldText.substring(offset + length));
			// Regex match against our pattern
			Matcher matcher = pattern.matcher(newText);
			if (!matcher.find()) {
				// Go ahead with the remove
				try {
					super.remove(fb, offset, length);
				} catch (BadLocationException e) {
					// Shouldn't happen
					e.printStackTrace();
				}
			}
		}
	}
	
	// Create Document listener for monitoring changes to the input box
	public class MyDocumentListener implements DocumentListener {
	    public void insertUpdate(DocumentEvent e) {
	        updateStats(e);
	    }
	    public void removeUpdate(DocumentEvent e) {
	    	updateStats(e);
	    }
	    public void changedUpdate(DocumentEvent e) {
	        //Unused
	    }
	    
	    // Calculate and display some statistics
	    private void updateStats(DocumentEvent e) {
	    	// Prevent explosions caused by empty fields
	    	if (!inputBox.getText().isEmpty()) {
	    		// Split the input by comma, convert to a list, sort
		    	// Also find sum and average in the process
	    		String[] numArray = inputBox.getText().split(",");
		    	List<Integer> numList = new LinkedList<Integer>();
		    	long sum = 0;
		    	double average = 0.0;
		    	for (String num : numArray) {
		    		int value = 0;
		    		try {
		    			value = Integer.valueOf(num);
		    		} catch (NumberFormatException nfe) {
		    			// Handle overflows on conversion
		    			if (num.length() > 9) {
		    				value = Integer.MAX_VALUE;
		    			} // Otherwise, leave it as zero
		    		}
		    		// Avoid a trailing minus sign
		    		if (num.endsWith("-")) {
		    			continue;
		    		} else {		    		
		    			numList.add(value);
		    			sum += value;
		    		}
		    	}
		    	
		    	// One last check, in case the data begins with a minus
		    	if (numList.isEmpty()) {
		    		blank();
		    		return;
		    	}
		    	
		    	// Calculate the sum, directly find the min and max 
		    	outputSum.setText("Sum: " + String.valueOf(sum));
		    	outputMax.setText("Max: " + String.valueOf(Collections.max(numList)));
		    	outputMin.setText("Min: " + String.valueOf(Collections.min(numList)));
		    	
		    	// Calculate the average
		    	average = (double)sum / (double)numList.size();
		    	outputAverage.setText("Average: " + String.valueOf(average));
		    	
		    	double median = 0.0;
		    	// Sort the list to determine the median
		    	Collections.sort(numList);
		    	if ((numList.size() & 1) == 1) {
		    		// Array size is odd, choose the middle value
		    		median = numList.get(numList.size() / 2);
		    	} else {
		    		// Array size is even, average the middle two values
		    		median = (double)(numList.get(numList.size() / 2) + numList.get((numList.size() / 2) - 1)) / 2.0;
		    	}
		    	outputMedian.setText("Median: " + String.valueOf(median));
	    	} else {
	    		// Blank the display
	    		blank();
	    	}
	    }
	}
	
	// Instantiate the GUI
	private static void initGUI() {
		// Prepare GUI container
		ArrayStatistics newFrame = new ArrayStatistics();
		newFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
		newFrame.setTitle("Integer Array Statistics");
		newFrame.pack();
		
		// Show the GUI instance
		newFrame.setVisible(true);
	}
	
	// Application entry point
	public static void main(String args[]) {
		javax.swing.SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                initGUI();
            }
        });
    }
}
